package com.google.code.newpath.aop;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import net.sf.cglib.proxy.Callback;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

import com.google.code.newpath.aop.annotation.Around;
import com.google.code.newpath.aop.annotation.Aspect;
import com.google.code.newpath.aop.config.Advice;
import com.google.code.newpath.aop.config.AspectConfig;
import com.google.code.newpath.aop.config.Pointcut;
import com.google.code.newpath.aop.impl.ProceedingJoinPointImpl;
import com.google.code.newpath.aop.impl.ProceedingJoinPointImpl.Processor;
import com.google.code.newpath.core.Context;
import com.google.code.newpath.core.Resource;
import com.google.code.newpath.core.ResourceFactory;
import com.google.code.newpath.core.ResourceInstanceEvent;
import com.google.code.newpath.core.ResourceInstanceProcessor;

public class AopInstanceEvent implements ResourceInstanceEvent<Object> {
	
	//store all aspect configurations.  Each AspectConfig represent one advice(method) and pointcut pair.
	private  List<AspectConfig> aspectConfigList = new LinkedList<AspectConfig>();

	public Object instanceTarget(ResourceInstanceProcessor<Object> processor,
			Context context, Resource<Object> resource) {

		Object target = processor.process(context);
		
		return aspectProcess(resource, target);
	}
	
	
	private Object aspectProcess(Resource<?> resource, Object target) {
		
		if(target != null) {
			
			//Find the aspect configuration for this target
			boolean isAspectTarget = loadAspectConfig(target); 

			//If not found the aspect configuration, this target will found the matching point cut and create the dynamic proxy.
			//(Aspect target could not be join point and be process by advicor method.)
			if(isAspectTarget == false) { 
				
				Map<Method, List<Advice>> adviceMap = new HashMap<Method, List<Advice>>();
				for(AspectConfig aspectConfig : aspectConfigList) {
					if(aspectConfig.isMatchByPath(resource.getPath())) {
						
						Method[] methods = target.getClass().getMethods();
						for(Method method : methods) {
							if(aspectConfig.isMatchByMethod(method)) {
								List<Advice> advices = adviceMap.get(method);
								if(advices == null) {
									advices = new ArrayList<Advice>();
									adviceMap.put(method, advices);
								}
								advices.add(aspectConfig.getAdvice());
							}
						}
					}
				}
				
				if(adviceMap.size() > 0) {
					return getProxy(adviceMap, resource, target);
				}
				
			}
		}	
		
		return target;
	}
	
	/**
	 * <p>Load aspect configurations, if the target has been found corresponding aspect configurations. </p>
	 * <p>The advice method must be public method. Any private and protected method will be ignored.</p
	 * @param target
	 * @return  return true as aspect target, false as none aspect target
	 */
	private boolean loadAspectConfig(Object target) {
		boolean isAspectTarget = false;
		
		//Parse the annotation of target. If it contains aspect configurations, they will be loaded and stored in 
		if (null != target.getClass().getAnnotation(Aspect.class)) {
			isAspectTarget = true;
			Method[] adviceMethods = target.getClass().getMethods();
			for (Method adviceMethod : adviceMethods) {
		
				Annotation[] anns = adviceMethod.getAnnotations();
				for (Annotation ann : anns) {
					if(ann instanceof Around){	
						//Build the Pointcut instance
						Around around = (Around)ann;
						String[] includePaths = around.includePaths().split(",");
						String[] excludePaths = around.excludePaths().split(",");
						String[] methods = around.methods().split(",");

						Pointcut pointcut = new Pointcut(includePaths, excludePaths, methods);
						
						//Build the advice instance
						Advice advice = new Advice(target, adviceMethod);
						
						AspectConfig aspectConf = new AspectConfig(advice, pointcut);
						aspectConfigList.add(aspectConf);
					}
				}
			}
		}
		
		return isAspectTarget;
	}
	
	
	private Object getProxy(Map<Method, List<Advice>> adviceMap, Resource<?> resource, Object target) {

		Class<?> targetClass = target.getClass();
		ClassLoader classLoader = targetClass.getClassLoader();
		
		// Configure CGLIB Enhancer...
		Enhancer enhancer = new Enhancer();
		enhancer.setClassLoader(classLoader);
		enhancer.setSuperclass(targetClass);

		enhancer.setCallbacks(new Callback[]{new CGlibMethodInterceptor(adviceMap, resource, target)});

		Object proxy = enhancer.create();
		return proxy;

	}

	
	
	/**
	 * 
	 * General purpose AOP callback. 
	 */
	private class CGlibMethodInterceptor implements MethodInterceptor {

		private Map<Method, List<Advice>> adviceMap;
		
		private Resource<?> resource;
		
		private Object target;
		
		public CGlibMethodInterceptor(Map<Method, List<Advice>> adviceMap, Resource<?> resource, Object target) {
			this.adviceMap = adviceMap;
			this.resource = resource;
			this.target = target;
		}
		
		
		public Object intercept(Object enhancedObject, final Method targetMethod, final Object[] targetArgs,
				MethodProxy targetMethodProxy) throws Throwable {

			Object returnValue = null;
			List<Advice> advices = adviceMap.get(targetMethod);
			
			if(advices != null && advices.size() > 0) { //If found one or more advice on this method
				final Iterator<Advice> adivceIt = advices.iterator();
				Advice topAdvice = adivceIt.next();
				
				ProceedingJoinPointImpl joinPoint = new ProceedingJoinPointImpl(resource, target, enhancedObject, targetMethod, targetArgs);
				joinPoint.setProcessor(new Processor() { 
					public Object proceed(ProceedingJoinPointImpl thisJoinpoint, Object[] args) throws Throwable {
						if(adivceIt.hasNext()) {
							Advice nextAdvice = adivceIt.next();
							return nextAdvice.getAdviceMethod().invoke(nextAdvice.getAspectInstance(), thisJoinpoint);
						}
						else {
							return targetMethod.invoke(target, args);
						}
					}
				});
				
				returnValue = topAdvice.getAdviceMethod().invoke(topAdvice.getAspectInstance(), joinPoint);	
			}
			else {
				returnValue = targetMethodProxy.invoke(target, targetArgs);
			}
			return returnValue;
		}
		
	}
	

}
